package com.pf.controller;

import com.google.common.util.concurrent.RateLimiter;
import com.pf.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * 本案例是解决超卖现象的解决方式
 * <p>
 * 1.利用数据库乐观锁解决
 * 2.利用令牌桶+乐观锁解决（令牌桶只是起个限流作用）
 * 3.利用redisson分布式锁解决（底层实际上是使用redis）
 *
 * @author PanFei
 * @date 2021/5/20 21:28
 * @Version 1.0
 */
@Slf4j
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 创建5个令牌
     */
    private RateLimiter rateLimiter = RateLimiter.create(5);

    @Autowired
    private RedissonClient redissonClient;


    /**
     * 会出现超卖现象
     * 使用传统的方式：
     * 现象：商品库存有20个，每秒并发50个线程，结果出现了48个订单。超买了28个商品
     *
     * @param id
     * @return
     */
    @GetMapping("/killSimple/{id}")
    public String killSimple(@PathVariable("id") Integer id) {
        log.info(">>>>>>>>> 利用传统方式实现秒杀，会出现超卖现象........");
        try {
            orderService.killGoodsSimple(id);
            log.info("秒杀成功！");
        } catch (Exception e) {
            log.error("秒杀失败！", e);
        }
        return "执行结束！";
    }

    /**
     * 解决方式1：使用乐观锁方式解决超卖现象
     * 利用version字段，使用数据库乐观锁
     * 现象：商品库存20个，每秒并发50个线程，结果出现20个订单。正常
     *
     * @param id
     * @return
     */
    @GetMapping(value = "killWithCAS/{id}")
    public String killWithCAS(@PathVariable("id") int id) {
        log.info(">>>>>>>>> 利用乐观锁方式实现秒杀，解决超卖现象........");
        try {
            orderService.killWithCAS(id);
            log.info("秒杀成功！");
        } catch (Exception e) {
            log.error("秒杀失败！", e);
        }
        return "执行结束！";
    }

    /**
     * 会出现超卖现象：
     * 仅仅使用令牌桶是不能解决超卖现象，令牌桶只是起个限流的作用
     *
     * @param id
     * @return
     */
    @GetMapping(value = "killWithLimter/{id}")
    public String killWithLimter(@PathVariable("id") int id) {
        log.info(">>>>>>>>> 利用令牌桶方式实现秒杀，不能解决超卖现象........");
        try {
            //令牌桶算法第一种策略：如果此次请求获取到了token令牌，则处理业务。如果此次请求未获取到token令牌，则一直等待，等获取到token令牌后，再处理业务。
            //rateLimiter.acquire();
            //令牌通算法第二种策略：设置一个等待时间，如果在等待的时间内获取到了token令牌，则处理业务。如果在等待时间内没有获取到token令牌则跑批此次请求
            //获取令牌后，则返回true，否则返回false
            if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {
                log.info("当前请求被限流，直接抛弃，无法调用后续秒杀逻辑...");
                return "抢购失败！";
            }
            orderService.killGoodsSimple(id);
            log.info("秒杀成功！");
        } catch (Exception e) {
            log.error("秒杀失败！", e);
        }
        return "执行结束！";
    }

    /**
     * 解决方式2：令牌桶+乐观锁解决超卖现象
     *
     * @param id 商品库存id
     * @return
     */
    @GetMapping(value = "killWithCASLimter/{id}")
    public String killWithCASLimter(@PathVariable("id") int id) {
        log.info(">>>>>>>>> 利用乐观锁+令牌桶方式实现秒杀，解决超卖现象........");
        try {
            //令牌桶算法第一种策略：如果此次请求获取到了token令牌，则处理业务。如果此次请求未获取到token令牌，则一直等待，等获取到token令牌后，再处理业务。
            //rateLimiter.acquire();
            //令牌通算法第二种策略：设置一个等待时间，如果在等待的时间内获取到了token令牌，则处理业务。如果在等待时间内没有获取到token令牌则跑批此次请求
            //获取令牌后，则返回true，否则返回false
            if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {
                log.info("当前请求被限流，直接抛弃，无法调用后续秒杀逻辑...");
                return "抢购失败！";
            }
            orderService.killWithCAS(id);
            log.info("秒杀成功！");
        } catch (Exception e) {
            log.error("秒杀失败！", e);
        }
        return "执行结束！";
    }

    /**
     * 解决方式3：利用redisson分布式锁解决超卖现象
     *
     * @param id
     * @return
     */
    @GetMapping(value = "killWithRedisson/{id}")
    public String killWithRedisson(@PathVariable("id") int id) {
        log.info(">>>>>>>>> 使用redisson方式实现秒杀，解决超卖现象...............");

        //获取redis锁
        String lockKey = "redissonKey";
        RLock lock = redissonClient.getLock(lockKey);
        try {
            //默认锁的有效期为30秒，假设程序执行时间超过30秒，redisson内部有个子线程会监视，如果发现30秒内程序未执行完，会不停的给锁续命
            //加锁
            lock.lock();
            orderService.killGoodsSimple(id);
            log.info("秒杀成功！");
        } catch (Exception e) {
            log.error("秒杀失败！", e);
        } finally {
            //解锁：删除锁
            lock.unlock();
        }
        return "执行结束！";
    }
}
